Assignment 4: Local Features and Image Matching

Tổng quan: Ở bài tập này, chúng ta sẽ thực hành lập trình một số hàm trích xuất đặc trưng ảnh cơ bản, sau đó ứng dụng cho các bài toán tạo ảnh panorama và bài toán đối sánh để tìm kiếm ảnh.

  • Harris Corner
  • Histogram of Oriented Gradients (HOG)
  • Difference of Gaussians (DoG)
  • Scale-Invariant Feature Transform (SIFT)

Yêu cầu thư viện: OpenCV 3.3, matplotlib, skimage

In [1]:
from IPython.display import Image
from skimage.feature import hog

import skimage
import matplotlib.pyplot as plt
import os
import cv2
import numpy as np

I. Harris Corner Detector

Harris Corner là một phương pháp phát hiện các điểm (có tính chất) góc trong ảnh, thường được sử dụng khi tính toán các đặc trưng ảnh cho các bài toán thị giác máy tính.

Thư viện OpenCV cung cấp hàm cv2.cornerHarris() để phát hiện các điểm góc trong ảnh. Tham số như sau:

  • img - Input image, it should be grayscale and float32 type.
  • blockSize - It is the size of neighbourhood considered for corner detection
  • ksize - Aperture parameter of Sobel derivative used.
  • k - Harris detector free parameter in the equation.

https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_features_harris/py_features_harris.html

Hãy hoàn thành hàm detect_corner(), với các tham số sau:

Input:

  • image_path: Đường dẫn file ảnh
  • blockSize, ksize, k: tham số của hàm cv2.cornerHarris()
  • threshold: ngưỡng để coi 1 điểm là góc

Output:

  • Đường dẫn tới file ảnh đầu ra
In [2]:
def detect_corner(image_path, blockSize=2, ksize=3, k=0.04, threshold=0.01):
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

    gray = np.float32(gray)
    dst = cv2.cornerHarris(gray,blockSize,ksize,k)

    #result is dilated for marking the corners, not important
    dst = cv2.dilate(dst,None)

    # Threshold for an optimal value, it may vary depending on the image.
    img[dst>threshold*dst.max()]=[0,0,255]
    
    cv2.imwrite('corner_' + image_path,img)
    return 'corner_' + image_path
In [3]:
out_path = detect_corner('chessboard.jpg')
# out_path = detect_corner('sudoku.png')
Image(out_path)
Out[3]:

Hãy thử tùy chỉnh tham số của hàm detect_corner() với ảnh 'house.jpg' sau sao cho ảnh đầu ra phát hiện được nhiều góc chính xác nhất.

In [4]:
Image('sudoku.png')
Out[4]:
In [5]:
### YOUR CODE HERE ###
out_path = None
pass
### YOUR CODE HERE ###
Image(out_path)
Out[5]:
In [6]:
Image('house.jpg')
Out[6]:
In [7]:
### YOUR CODE HERE ###
out_path = None
pass
### YOUR CODE HERE ###

Image(out_path)
Out[7]:

II. Histogram of Oriented Gradients (HOG)

Histogram of Oriented Gradients (HOG) là bộ mô tả đặc trưng thường được sử dụng trong thị giác máy tính và xử lí ảnh để biểu diễn đối tượng trong ảnh

Thư viện scikit-image (skimage) cung cấp hàm skimage.feature.hog() để trích chọn đặc trưng HOG. Một số tham số quan trọng của hàm như sau:

  • orientations: Number of orientation bins.
  • pixels_per_cell: Size (in pixels) of a cell.
  • cells_per_block: Number of cells in each block.

Xem đầy đủ tại: http://scikit-image.org/docs/dev/api/skimage.feature.html#skimage.feature.hog

Tutorial chi tiết về HOG: https://www.learnopencv.com/histogram-of-oriented-gradients/

In [8]:
image = cv2.imread('man.png')

fd, hog_image = hog(image, orientations=8, pixels_per_cell=(16, 16),
                    cells_per_block=(1, 1), visualize=True, multichannel=True)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4), sharex=True, sharey=True)

ax1.axis('off')
ax1.imshow(image, cmap=plt.cm.gray)
ax1.set_title('Input image')

# Rescale histogram for better display
hog_image_rescaled = skimage.exposure.rescale_intensity(hog_image, in_range=(0, 10))

ax2.axis('off')
ax2.imshow(hog_image_rescaled, cmap=plt.cm.gray)
ax2.set_title('Histogram of Oriented Gradients')
plt.show()
/usr/local/lib/python3.5/dist-packages/skimage/feature/_hog.py:150: skimage_deprecation: Default value of `block_norm`==`L1` is deprecated and will be changed to `L2-Hys` in v0.15. To supress this message specify explicitly the normalization method.
  skimage_deprecation)

IV. Scale-Invariant Feature Transform (SIFT)

Thư viện OpenCV cung cấp các hàm liên quan đến trích chọn đặc trưng SIFT.

https://docs.opencv.org/3.4/d5/d3c/classcv_1_1xfeatures2d_1_1SIFT.html

Để khởi tạo đối tượng SIFT trong OpenCV ta sử dụng lệnh: sift = cv2.xfeatures2d.SIFT_create().

Đối tượng này có phương thức detectAndCompute trả về 2 outputs kp và des, kp là một list chứa các keypoints được detect bởi SIFT, des là một numpy array chứa len(kp) vectors 128 chiều.

Chúng ta sẽ dùng các des này để phục vụ bài toán phân loại.

In [9]:
img = cv2.imread('ville01002.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

sift = cv2.xfeatures2d.SIFT_create()

kp, des = sift.detectAndCompute(gray,None)

img=cv2.drawKeypoints(gray,kp,img)
cv2.imwrite('sift_keypoints.jpg',img)
Image('sift_keypoints.jpg')
Out[9]:

IV. Interest point detector

Speeded-Up Robust Features (SURF)

In [10]:
img = cv2.imread('box.png', 0)

minHessian = 400
detector = cv2.xfeatures2d_SURF.create(hessianThreshold=minHessian)
keypoints = detector.detect(img)
#-- Draw keypoints
img_keypoints = np.empty((img.shape[0], img.shape[1], 3), dtype=np.uint8)
cv2.drawKeypoints(img, keypoints, img_keypoints)

cv2.imwrite('surf_keypoints.jpg',img_keypoints)
Image('surf_keypoints.jpg')
Out[10]:

V. Panorama Stitching

In [11]:
imageA = cv2.imread('./panorama/mountain1_left.png')
imageB = cv2.imread('./panorama/mountain1_right.png')

stitcher = cv2.createStitcher(False)
result = stitcher.stitch((imageA, imageB))

cv2.imwrite('mountain1.png', result[1])
Image('mountain1.png')
Out[11]:
In [12]:
imageA = cv2.imread('./panorama/scottsdale_left.png')
imageB = cv2.imread('./panorama/scottsdale_right.png')

stitcher = cv2.createStitcher(False)
result = stitcher.stitch((imageA, imageB))

cv2.imwrite('scottsdale.png', result[1])
Image('scottsdale.png')
Out[12]:
In [13]:
def detectAndDescribe(image):
    # convert the image to grayscale
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # detect and extract features from the image
    descriptor = cv2.xfeatures2d.SIFT_create()
    (kps, features) = descriptor.detectAndCompute(image, None)

    # convert the keypoints from KeyPoint objects to NumPy arrays
    kps = np.float32([kp.pt for kp in kps])

    # return a tuple of keypoints and features
    return (kps, features)
In [14]:
def matchKeypoints(kpsA, kpsB, featuresA, featuresB, ratio, reprojThresh):
    # compute the raw matches and initialize the list of actual
    # matches
    matcher = cv2.DescriptorMatcher_create("BruteForce")
    rawMatches = matcher.knnMatch(featuresA, featuresB, 2)
    matches = []

    # loop over the raw matches
    for m in rawMatches:
        # ensure the distance is within a certain ratio of each
        # other (i.e. Lowe's ratio test)
        if len(m) == 2 and m[0].distance < m[1].distance * ratio:
            matches.append((m[0].trainIdx, m[0].queryIdx))

    # computing a homography requires at least 4 matches
    if len(matches) > 4:
        # construct the two sets of points
        ptsA = np.float32([kpsA[i] for (_, i) in matches])
        ptsB = np.float32([kpsB[i] for (i, _) in matches])

        # compute the homography between the two sets of points
        (H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC,
            reprojThresh)

        # return the matches along with the homograpy matrix
        # and status of each matched point
        return (matches, H, status)

    # otherwise, no homograpy could be computed
    return None
In [15]:
def drawMatches(imageA, imageB, kpsA, kpsB, matches, status):
    # initialize the output visualization image
    (hA, wA) = imageA.shape[:2]
    (hB, wB) = imageB.shape[:2]
    vis = np.zeros((max(hA, hB), wA + wB, 3), dtype="uint8")
    vis[0:hA, 0:wA] = imageA
    vis[0:hB, wA:] = imageB

    # loop over the matches
    for ((trainIdx, queryIdx), s) in zip(matches, status):
        # only process the match if the keypoint was successfully
        # matched
        if s == 1:
            # draw the match
            ptA = (int(kpsA[queryIdx][0]), int(kpsA[queryIdx][1]))
            ptB = (int(kpsB[trainIdx][0]) + wA, int(kpsB[trainIdx][1]))
            cv2.line(vis, ptA, ptB, (0, 255, 0), 1)

    # return the visualization
    return vis
In [16]:
def stitch(images, ratio=0.75, reprojThresh=4.0, showMatches=False):
    # unpack the images, then detect keypoints and extract
    # local invariant descriptors from them
    (imageB, imageA) = images
    (kpsA, featuresA) = detectAndDescribe(imageA)
    (kpsB, featuresB) = detectAndDescribe(imageB)

    # match features between the two images
    M = matchKeypoints(kpsA, kpsB, featuresA, featuresB, ratio, reprojThresh)

    # if the match is None, then there aren't enough matched
    # keypoints to create a panorama
    if M is None:
        return None

    # otherwise, apply a perspective warp to stitch the images
    # together
    (matches, H, status) = M
    result = cv2.warpPerspective(imageA, H, (imageA.shape[1] + imageB.shape[1], imageA.shape[0]))
    result[0:imageB.shape[0], 0:imageB.shape[1]] = imageB

    # check to see if the keypoint matches should be visualized
    if showMatches:
        vis = drawMatches(imageA, imageB, kpsA, kpsB, matches, status)

        # return a tuple of the stitched image and the
        # visualization
        return (result, vis)

    # return the stitched image
    return result
In [17]:
imageA = cv2.imread('./panorama/mountain1_left.png')
imageB = cv2.imread('./panorama/mountain1_right.png')

# stitch the images together to create a panorama
(result, vis) = stitch([imageA, imageB], showMatches=True)
In [18]:
cv2.imwrite('matching.png', vis)
Image('matching.png')
Out[18]:
In [19]:
cv2.imwrite('result.png', result)
Image('result.png')
Out[19]:

VI. Image Matching

In [20]:
img1 = cv2.imread('thaprua.jpg')
img2 = cv2.imread('thaprua2.jpg')
img3 = cv2.imread('chuamotcot.jpg')
img4 = cv2.imread('cotco.jpg')
In [21]:
img1_ = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)
img2_ = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)
img3_ = cv2.cvtColor(img3, cv2.COLOR_BGR2RGB)
img4_ = cv2.cvtColor(img4, cv2.COLOR_BGR2RGB)
plt.subplot(231), plt.imshow(img1_)
plt.subplot(234), plt.imshow(img2_)
plt.subplot(235), plt.imshow(img3_)
plt.subplot(236), plt.imshow(img4_)
Out[21]:
(<matplotlib.axes._subplots.AxesSubplot at 0x7f65d99900f0>,
 <matplotlib.image.AxesImage at 0x7f65d9965160>)
In [22]:
sift = cv2.xfeatures2d.SIFT_create()
In [23]:
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)
kp3, des3 = sift.detectAndCompute(img3,None)
kp4, des4 = sift.detectAndCompute(img4,None)
In [24]:
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks=50)
In [25]:
flann = cv2.FlannBasedMatcher(index_params,search_params)
In [26]:
matches2 = flann.knnMatch(des1,des2,k=2)
matches3 = flann.knnMatch(des1,des3,k=2)
matches4 = flann.knnMatch(des1,des4,k=2)
In [27]:
cnt = 0
matchesMask = [[0,0] for i in range(len(matches2))]
for i,(m,n) in enumerate(matches2):
    if m.distance < 0.7*n.distance:
        matchesMask[i]=[1,0]
        cnt += 1
        
print("Number of matchesMask: " + str(cnt))

draw_params = dict(matchColor = (0,255,0),
                   singlePointColor = (255,0,0),
                   matchesMask = matchesMask,
                   flags = 0)

imgRes = cv2.drawMatchesKnn(img1,kp1,img2,kp2,matches2,None,**draw_params)
plt.imshow(imgRes,),plt.show()
Number of matchesMask: 237
Out[27]:
(<matplotlib.image.AxesImage at 0x7f65d5688470>, None)
In [28]:
cnt = 0
matchesMask = [[0,0] for i in range(len(matches3))]
for i,(m,n) in enumerate(matches3):
    if m.distance < 0.7*n.distance:
        matchesMask[i]=[1,0]
        cnt += 1
        
print("Number of matchesMask: " + str(cnt))

draw_params = dict(matchColor = (0,255,0),
                   singlePointColor = (255,0,0),
                   matchesMask = matchesMask,
                   flags = 0)

imgRes = cv2.drawMatchesKnn(img1,kp1,img3,kp3,matches3,None,**draw_params)
plt.imshow(imgRes,),plt.show()
Number of matchesMask: 7
Out[28]:
(<matplotlib.image.AxesImage at 0x7f65d56b55f8>, None)
In [29]:
cnt = 0
matchesMask = [[0,0] for i in range(len(matches4))]
for i,(m,n) in enumerate(matches4):
    if m.distance < 0.7*n.distance:
        matchesMask[i]=[1,0]
        cnt += 1
print("Number of matchesMask: " + str(cnt))

draw_params = dict(matchColor = (0,255,0),
                   singlePointColor = (255,0,0),
                   matchesMask = matchesMask,
                   flags = 0)

imgRes = cv2.drawMatchesKnn(img1,kp1,img4,kp4,matches4,None,**draw_params)
plt.imshow(imgRes,),plt.show()
Number of matchesMask: 2
Out[29]:
(<matplotlib.image.AxesImage at 0x7f65d566f080>, None)